Part2 Ringについて知る
Part1 Webアプリケーションはじめの一歩では、サーバーを起動させて「Hello, world!」をブラウザに表示させました。このPartでは、Part1でも使っていたClojureでWebアプリケーションを作る上で必須となるRingについて説明していきます。Ringの仕様に関する理解はWebアプリケーションの開発中やトラブルシューティングの際に役に立つので、ここでしっかりと理解していきます。 RingはPythonでいうWSGI、RubyでいうRackのようなものです、と言ってもPythonやRubyを触ったことがない方はよく分からないと思うので順を追って説明します。Ringの目的はHTTPの詳細、つまりリクエストやレスポンスの情報を、シンプルで統一された抽象をユーザーに提供することにあります。これは実際にはリクエストとレスポンスをClojureのマップの形にすることで実現されています。また、Ringの仕様に沿ってミドルウェアというモジュラーなコンポーネントをユーザーは定義することができ、それらを利用してWebアプリケーションを構築することができるようになっています。 Ringには5つのコンポーネントがある
先程ミドルウェアという言葉が出てきましたが、Ringにはミドルウェアを含めて次の5つのコンポーネントが存在しています。
アダプター
ハンドラー
ミドルウェア
リクエストマップ
レスポンスマップ
これらは単に「ハンドラー」のように呼ぶこともありますし、Ringのものであることを強調するために「Ringハンドラー」というような呼び方をすることもあります。これからこれら5つのコンポーネントについて紹介していきます。
アダプターはHTTPとRingアプリケーションの境界線
アダプターはその名の通り、HTTPとRingハンドラを仲介するものです。アダプターはクライアントからHTTPリクエストを受けとり、それをリクエストマップへと変換しハンドラーへと渡します。そして、ハンドラーが返却してきたレスポンスマップをHTTPレスポンスへと変換しクライアントへと返却します。一般に、ユーザーはパフォーマンスチューニングをしたいなどの理由がない限り、アダプターの詳細を意識する必要はありません。
ハンドラーでリクエストを受け取り、レスポンスを返す
既に述べた通り、Ringはクライアントからのリクエストとサーバーからのレスポンスを表現するのに特別なクラスやプロトコルなどを設けず、Clojureのマップを利用しています。ハンドラーはリクエストを受け取りレスポンスを返すただの関数です。例えば、シンプルなハンドラーは次のように書けます。
code: (clojure)
{:status 200
:headers {"Content-Type" "text/plain"}
:body (:remote-addr req)})
このハンドラーはステータスコードを200として、クライアントのIPアドレスをただのテキストとして返却しています。このハンドラーではマップリテラルを用いて直接マップを返却していますが、最終的にRingの仕様に沿ったレスポンスマップが返却できれば良いため、次のようにマップを生成して返却するようなコードを書くことも可能です。
code: (clojure)
(-> (res/response (:remote-addr req))
(res/header "Content-Type" "text/plain")))
またハンドラーと言っても本質的には、ただのClojureの関数なので次のように簡単に評価することができます。
code: (clojure)
user> (handler {:remote-addr "localhost"})
;; => {:status 200,
;; :headers {"Content-Type" "text/plain"},
;; :body "localhost"}
簡単に評価することができるということは、これに対するテストも簡単に書けるということです。この後のPartではRingミドルウェアなどが関わってきて、リクエストマップが変更されることもあるので全てがこのように簡単に評価できたりするわけではなかったりしますが、追って説明していくことにします。
リクエストマップとレスポンスマップ
前述の通りリクエストマップとレスポンスマップは、単純なClojureのマップであるということを踏まえた上で、それぞれについて少し詳しく見ていきましょう。
HTTPリクエストをClojureのマップで表現したものをリクエストマップと呼びます。リクエストマップは幾つかの標準的なキーが常に存在しますが、これら以外にもユーザーがミドルウェアを介して自由にキーを追加することができます。
table:標準的なリクエストマップに含まれるキー
キー 意味
:server-port リクエストをハンドルしたポート番号
:server-name 解決されたサーバー名もしくはIPアドレス
:remote-addr クライアントが最後にリクエストを投げたプロキシのIPアドレス
:uri リクエストURI(ドメイン以下のフルパス)
:query-string もしあれば、クエリ文字列
:schema トランスポートプロトコル:httpまたは:https
:request-method HTTPリクエストメソッド。:get,:head,:options,:put,:post,:deleteのいずれか
:headers ヘッダーの文字列を小文字化したキーを持つマップ
:body もしあれば、リクエストボディのためのInputStream
もし、既にClojureでWebアプリケーションを開発したことがあれば、おそらくこれら以外のキーを見たことがあるでしょう(例えば:paramsや:sessionなど)。それらはミドルウェアによって差し込まれたものですが、同様にそのようなミドルウェアを自分で作成して適用することもできます。ミドルウェアの作り方については後述することにします。
HTTPレスポンスをClojureのマップで表現したものをレスポンスマップと呼びます。ハンドラーが作成するレスポンスマップでは最低でも以下の3つのキーがあることを期待されます。
table:レスポンスマップに必要なキー
キー 意味
:status HTTPステータスコード
:heaers クライアントへと返すHTTPヘッダー
:body レスポンスボディ
ステータスコードはRFCで定義されているものと同じで200や404などというものです。 ヘッダーはHTTPヘッダーと同じ名前を使ったマップです。それぞれのバリューについて文字列か文字列のシーケンスを使うことができますが、文字列の場合はそのままHTTPレスポンスとして送信し、文字列のシーケンスの場合はそれぞれの値を送信します。
レスポンスボディは文字列、シーケンス、ファイル、ストリームのいずれかの型を利用することができます。
また、レスポンスマップとしては上記の3つのキーだけで十分ですが、リクエストマップと同様にミドルウェアにてキーを追加したりレスポンスボディに変更を加えたりすることができます(その場合、少々複雑になりますが)。
ミドルウェアを追加して機能を足す
ミドルウェアはハンドラーのための高階関数として定義されます。ミドルウェア関数は第一引数としてハンドラーを受け取り、新しいハンドラー関数を返さなければなりません。文字で説明されてもよく分からないと思うので、次の例を見てみましょう。
code: (clojure)
(defn wrap-exclamation-mark handler (update response :body #(str % "!!"))))) このシンプルなミドルウェア関数は、ハンドラーが作る全てのレスポンスのボディの最後に対してビックリマークを足すものです(実用性は特にありません)。
一見すると複雑そうに見えますが、冷静に見るととてもシンプルなものです。また、この例では古いハンドラーを評価して得たレスポンスマップに対して変更を加えていますが、リクエストマップに対して変更を加えるようなミドルウェアは次のように定義できます。
code: (clojure)
(defn wrap-parse-query-string handler (let [params (parse-query-string (:query-string request))
updated-request (assoc request :params params)]
(handler updated-request))))
parse-query-stringという関数はここでは存在するものとして扱いますが、名前の通りquery-stringをパースしてマップに変換するものだと思ってください。ハンドラーを受け取ったミドルウェアは新しいハンドラーを返しますが、その新しいハンドラーの中でリクエストマップを編集し更新したリクエストマップを古いハンドラーに渡すということをしています。
このようなミドルウェアは次のようにハンドラーへと適用します。
code: (clojure)
(def app
(wrap-parse-query-string (wrap-exclamation-mark handler)))
これは新しいハンドラーappをhandlerにwrap-exclamation-markを適用したものとして定義しています。
またスレッディングマクロ(->)を用いることで読みやすくできます。
code: (clojure)
(def app
(-> handler
wrap-exclamation-mark
wrap-parse-query-string))
Ringでは標準のミドルウェアを幾つか提供しているため、一般的なものであれば自分で定義する必要はありません。また標準以外にも沢山のライブラリがあるので、用途にあったものを探し適用することも可能です。